(ns server.dbmain 
	(:use server.readbinfile)
	(:import (java.util.regex Pattern))
	(:import (java.io File RandomAccessFile))
	(:use clojure.contrib.except)
)

;;------------------------------------------------------------------------------------------------------------------------------------

(defn map-a-map
	"Applies the function to the values of the map, and returns the new map
	functions --  the function to apply
	dictionary -- the map where the operations will be performed"
	[function dictionary]	
	(loop
		[keys-array (keys dictionary)
		new-dictionary dictionary]
		(if (first keys-array)
			(recur (rest keys-array) (assoc new-dictionary (first keys-array) 
				(function ((first keys-array) new-dictionary))))
			new-dictionary)))
		
;;------------------------------------------------------------------------------------------------------------------------------------
			
(defn off-first
	"Takes off the first character of a String.
	In case its empty, the returned String will be empty as well.
	a-string -- a String containing 0 or more characters"
	[a-string]
	(if (> (.length a-string) 0)
		(.substring a-string 1 (.length a-string))
		a-string
	)
)
	
;;------------------------------------------------------------------------------------------------------------------------------------

(defn matches-criteria 
	"Returns whether or not (a boolean) the map of records matches the map of
	criteria. A value is considered to pass the criteria if the record
	contains the same value as the criteria, or begins with the same characters.
	E.g. Petter matches Pet and Petter and Petterer, but does not match Pat nor Patter
	the fields and values contained in critera may be same or less than those in record.
	In any case, only those keys that match will be evaluated.
	record -- a map with the form (:field value}
	criteria -- a map with the form {:field value}"
	[record criteria]
	(let [regex (map-a-map 
				#(if (= (first %1) \$)
					(str (off-first %1) ".*")
					(str %1 ".*"))
				criteria)]					
	(loop
		[record record
		regex regex
		keys-array (keys regex)]
		(if (first keys-array)
			(if (= (first keys-array) :rate) ;the special case of rate
				
				(if (Pattern/matches ((first keys-array) regex)
					(off-first ((first keys-array) record)))
					(recur record regex (rest keys-array))
				false)
				
				(if (Pattern/matches ((first keys-array) regex) 
					((first keys-array) record))
					(recur record regex (rest keys-array))
				false)
			)
		true))))

;;------------------------------------------------------------------------------------------------------------------------------------
		
(defn find-in-db
	"Finds the record numbers that match the specified criteria in the specified file 
	and returns a list of them. Equivalent to the method find.
	criteria -- a map with the form {:field value}, where value is the criteria 
	corresponding to the field that will be evaluated.
	filename -- the name of the database where the search will be performed"
	[criteria
	 filename]
	(let [db (read-bin-file filename)
		 	records (:records db)
		 	matching (list)]
		 (loop
		 	 [n 0
		 	 remaining records
		 	 passed-criteria matching]
		 	 (if (first remaining)
		 	 	(if (matches-criteria (first remaining) criteria)
		 	 		(recur (inc n) (rest remaining) (cons n passed-criteria))
		 	 	(recur (inc n) (rest remaining) passed-criteria))
			 passed-criteria))))

;;------------------------------------------------------------------------------------------------------------------------------------
			 
(defn read-record
	"Reads a record from the file and returns a map where
	each element is a record value and its key is the field name
	file -- the database where the record will be read from 
	rec-number -- the number of the record in the database expected to be read"
	[file rec-number]
	(get (:records (read-bin-file file)) rec-number))

;;------------------------------------------------------------------------------------------------------------------------------------
	
(defn move-n-records
	"Moves the file pointer n records
	file -- the file from which the pointer will be obtained
	fields --the fields that the database's schema has
	n -- the number of records that the pointer will be moved"
	[file fields n]
	(loop [number n
		   ror    nil]
		(if (zero? (- (.length file) (.getFilePointer file))) 
			fields ;reached EOF
		(if (zero? number)
			fields ;reached the desired record
		(recur (dec number) (read-one-record file fields))))))
	
;;------------------------------------------------------------------------------------------------------------------------------------
		
(defn move-pointer 
	"Moves the file pointer to the indicated rec-no
	rafile -- a Random Acces File where the pointer will be moved
	rec-no -- the record number where the pointer wants to be moved to"
	[rafile rec-no]
	(let [magic-number (.readInt rafile)
          offset       (.readInt rafile)
          num-fields   (.readShort rafile)
          fields-meta  (read-fields-meta rafile num-fields)]
	(move-n-records rafile fields-meta rec-no)))

;;------------------------------------------------------------------------------------------------------------------------------------
	
(defn delete-record
	"Deletes the specified record number in the specified file
	filename -- the database name where the record will be erased
	rec-no -- the number of the record that will be erased"
	[filename rec-no]
	(with-open [rafile (RandomAccessFile. filename "rw") ]
		(move-pointer rafile rec-no)
		(.writeShort rafile 0x8000)))
		
;;------------------------------------------------------------------------------------------------------------------------------------
		
(defn update-one-value
	"Same as update-record, but updates just one data at a time
	the arguments are therefore documented in update-record"
	[filename rec-no data]
	(with-open [rafile (RandomAccessFile. filename "rw") ]
	(let [fields (move-pointer rafile rec-no)]
		(.writeShort rafile 00)
		(loop [
			[[fname fsize] & rest-fields] fields
			been-read nil]
			(if fname
			 	(if (contains? data (keyword fname))
			 		(let [llave (keyword fname)]
			 			(.write rafile (byte-array fsize (.getBytes (llave data)))))
			 	(recur rest-fields (read-str-len rafile fsize)))
			 nil)))))
			 
;;------------------------------------------------------------------------------------------------------------------------------------

(defn update-record
	"Updates a record's values according to the given data
	filename -- the database name where the record will be updated
	rec-no -- the number of record that will be updated
	data -- a map with the for {:field value}, containig the new values for the record"
	[filename rec-no data]
	(loop [
		some-data nil
		data-keys (keys data)]
		(if (first data-keys)
			(recur (update-one-value filename rec-no {(first data-keys) ((first data-keys) data)}) (rest data-keys))
		rec-no)))
		
;;------------------------------------------------------------------------------------------------------------------------------------

(defn write-one-value
	"Writes just one value for the new record
	filename -- the database name, where the value will be written
	value -- a String with the data to be written
	size -- a number indicating the size of the value in bytes"
	[filename value size]
	(.write filename (byte-array size (.getBytes value))))
	
;;------------------------------------------------------------------------------------------------------------------------------------

(defn create-record
	"Creates a new record in the database and returns its record number.
	It may occupy a deleted record, or create a new one at the end.
	filename -- the name of the database where the record will be created
	data -- a map with the form {:field value}, containing the data of the new record"
	[filename data]
	(let [erased (first (find-in-db {:deleted "true"} filename))]
		(if (nil? erased)
			;then write at the end of the file
			(with-open [rafile (RandomAccessFile. filename "rw")]
				(let
					[magic-number (.readInt rafile)
					offset        (.readInt rafile)
					num-fields    (.readShort rafile)
					fields-meta   (read-fields-meta rafile num-fields)]
					(.seek rafile 0)
					(.skipBytes rafile (.intValue (.length rafile)))
					(.writeShort rafile 00)
					(loop [ some-data nil
						[[fname fsize] & rest-fields] fields-meta]
						(if fname
							(recur (write-one-value rafile ((keyword fname) data) fsize) rest-fields)
				(first (find-in-db {:deleted "false"} filename)))))
			)
			(update-record filename erased data))))

